﻿Imports System.IO
Imports System.Runtime.Serialization
Imports System.Runtime.Serialization.Formatters.Binary

<Serializable()>
Public Class RawLibrary
    Implements ISerializable

#Region "Serialization"

    Private Sub New(info As SerializationInfo, context As StreamingContext)
        Me.New()

        Dim version As Integer = info.GetInt32("version")

        If version >= 1 Then
            mSources = info.GetValue(Of List(Of RawSource))("sources")
            mIssues = info.GetValue(Of List(Of RawIssue))("issues")
            mGraphics = info.GetValue(Of Dictionary(Of String, GraphicsSet))("graphics")
            libraryName = info.GetString("libraryName")
            mObjects = info.GetValue(Of Dictionary(Of ObjectType, Dictionary(Of String, RawObject)))("objects")
        End If

        If version >= 3 Then
            mBodyGloss = info.GetValue(Of Dictionary(Of String, BodyGlossEntry))("bodyGloss")
        End If

        If version >= 4 Then
            mLanguage = info.GetValue(Of LanguageLookup)("language")
        Else
            mLanguage = New LanguageLookup
        End If
    End Sub


    Public Sub getObjectData(info As SerializationInfo, context As StreamingContext) Implements ISerializable.GetObjectData
        info.AddValue("version", 4)

        'Version 1 data
        info.AddValue("sources", mSources)
        info.AddValue("issues", mIssues)
        info.AddValue("graphics", mGraphics)
        info.AddValue("libraryName", libraryName)
        info.AddValue("objects", mObjects)

        'Version 3 data
        info.AddValue("bodyGloss", mBodyGloss)

        'Version 4 data
        info.AddValue("language", mLanguage)
    End Sub
#End Region

    Private mSources As List(Of RawSource)
    Private mIssues As List(Of RawIssue)
    Private mGraphics As Dictionary(Of String, GraphicsSet)
    Public Property ObjectDescriptors As Dictionary(Of ObjectType, RawObjectDescriptor) 'not serialized
    Public Property LibraryName As String
    Private mBodyGloss As Dictionary(Of String, BodyGlossEntry)
    Private mLanguage As LanguageLookup

    Private mObjects As Dictionary(Of ObjectType, Dictionary(Of String, RawObject))
    Private mBasePath As String
    Private mObjectPath As String
    Private mGraphicsPath As String

    Private mGraphicSearchList As List(Of String)

    Public Sub New()
        ObjectDescriptors = New Dictionary(Of ObjectType, RawObjectDescriptor)
        mObjects = New Dictionary(Of ObjectType, Dictionary(Of String, RawObject))
        mSources = New List(Of RawSource)
        mIssues = New List(Of RawIssue)
        mGraphics = New Dictionary(Of String, GraphicsSet)
        mBodyGloss = New Dictionary(Of String, BodyGlossEntry)
        mLanguage = New LanguageLookup

        AddDescriptor(New RawObjectDescriptor(ObjectType.TISSUE_TEMPLATE, "TISSUE_TEMPLATE", "tissue_template", Nothing, {ObjectSubType.TISSUE_TEMPLATE}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.INTERACTION, "INTERACTION", "interaction", Nothing, {ObjectSubType.INTERACTION}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.REACTION, "REACTION", "reaction", Nothing, {ObjectSubType.REACTION}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.PLANT, "PLANT", "plant", Nothing, {ObjectSubType.PLANT}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.MATERIAL_TEMPLATE, "MATERIAL_TEMPLATE", "material_template", Nothing, {ObjectSubType.MATERIAL_TEMPLATE}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.ENTITY, "ENTITY", "entity", Nothing, {ObjectSubType.ENTITY}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.DESCRIPTOR_COLOR, "DESCRIPTOR_COLOR", "descriptor_color", Nothing, {ObjectSubType.COLOR}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.DESCRIPTOR_PATTERN, "DESCRIPTOR_PATTERN", "descriptor_pattern", Nothing, {ObjectSubType.COLOR_PATTERN}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.DESCRIPTOR_SHAPE, "DESCRIPTOR_SHAPE", "descriptor_shape", Nothing, {ObjectSubType.SHAPE}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.CREATURE, "CREATURE", "creature", Nothing, {ObjectSubType.CREATURE}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.INORGANIC, "INORGANIC", "inorganic", Nothing, {ObjectSubType.INORGANIC}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.BODY, "BODY", "body", Nothing, {ObjectSubType.BODY, ObjectSubType.BODYGLOSS}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.BUILDING, "BUILDING", "building", Nothing, {ObjectSubType.BUILDING_WORKSHOP,
                                                                                                     ObjectSubType.BUILDING_FURNACE}))

        AddDescriptor(New RawObjectDescriptor(ObjectType.CREATURE_VARIATION, "CREATURE_VARIATION", "c_variation", Nothing, {ObjectSubType.CREATURE_VARIATION}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.BODY_DETAIL_PLAN, "BODY_DETAIL_PLAN", "b_detail_plan", Nothing, {ObjectSubType.BODY_DETAIL_PLAN}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.ITEM, "ITEM", "item", Nothing, {ObjectSubType.ITEM_AMMO,
                                                                                         ObjectSubType.ITEM_ARMOR,
                                                                                         ObjectSubType.ITEM_FOOD,
                                                                                         ObjectSubType.ITEM_GLOVES,
                                                                                         ObjectSubType.ITEM_HELM,
                                                                                         ObjectSubType.ITEM_INSTRUMENT,
                                                                                         ObjectSubType.ITEM_PANTS,
                                                                                         ObjectSubType.ITEM_SHIELD,
                                                                                         ObjectSubType.ITEM_SHOES,
                                                                                         ObjectSubType.ITEM_SIEGEAMMO,
                                                                                         ObjectSubType.ITEM_TOOL,
                                                                                         ObjectSubType.ITEM_TOY,
                                                                                         ObjectSubType.ITEM_TRAPCOMP,
                                                                                         ObjectSubType.ITEM_WEAPON}))

        AddDescriptor(New RawObjectDescriptor(ObjectType.LANGUAGE, "LANGUAGE", "language", Nothing, {ObjectSubType.WORD,
                                                                                                     ObjectSubType.TRANSLATION}))
        AddDescriptor(New RawObjectDescriptor(ObjectType.GRAPHICS, "GRAPHICS", "graphics", Nothing, {ObjectSubType.CREATURE_GRAPHICS,
                                                                                                     ObjectSubType.TILE_PAGE}))

        mGraphicSearchList = New List(Of String)({"DEFAULT", "STANDARD", "CHILD"})
    End Sub

    Public Sub ParseRawDirectory(directory As String)
        mBasePath = directory
        mGraphicsPath = IO.Path.Combine(mBasePath, "graphics")
        mObjectPath = Path.Combine(directory, "objects")
        If IO.Directory.Exists(mObjectPath) Then
            ParseDirectory(mObjectPath)
        End If
        If IO.Directory.Exists(mGraphicsPath) Then
            ParseDirectory(mGraphicsPath)
        End If
    End Sub

    Private Sub ParseDirectory(directory As String)
        For Each descriptor As RawObjectDescriptor In ObjectDescriptors.Values
            For Each fileName As String In IO.Directory.GetFiles(directory, descriptor.BaseFileName & "*.txt")
                If fileName.ToLower.EndsWith(".txt") Then 'Yes, we really have to check to make sure it ends with just ".txt".  Searching for *.txt on the previous line will return things like "file1.txtblah". Yay Microsoft!
                    ParseFile(fileName)
                End If
            Next
        Next
    End Sub

    Private Sub ParseFile(filename As String)
        Dim reader As New StreamReader(filename)
        Dim file As String = reader.ReadLine
        If Path.GetFileNameWithoutExtension(filename) <> file Then
            Dim issue As New RawIssue
            issue.Severity = IssueSeverity.Warning
            issue.Text = "Incorrect file name marker in """ & DfHelper.MakeRelativePath(mBasePath, filename) & """"
        End If

        Dim rawReader As New RawReader(filename)

        If rawReader.ReadNextToken() Then
            Dim token As Token = rawReader.CurrentToken

            'Debug.WriteLine(filename)
            'If IO.Path.GetFileNameWithoutExtension(filename) = "language_SERPENTFOLK" Then
            '    Stop
            'End If

            Dim objectType As ObjectType
            'objectType = CType([Enum].Parse(GetType(ObjectType), token.arguments(0), True), ObjectType)
            If [Enum].TryParse(token.Arguments(0), True, objectType) Then
                Dim source As New RawSource(DfHelper.MakeRelativePath(mBasePath, filename), Me, objectType)
                mSources.Add(source)

                Dim descriptor As RawObjectDescriptor = ObjectDescriptors(objectType)
                If objectType = descriptor.ObjectType Then
                    Dim obj As RawObject = Nothing
                    Dim skipObject As Boolean = True
                    Do While rawReader.ReadNextToken
                        Dim subType As ObjectSubType? = GetSubTypeFromString(rawReader.CurrentToken.Name)
                        If subType.HasValue AndAlso descriptor.ObjectClasses.Contains(subType.Value) Then
                            'New object class token
                            If obj IsNot Nothing Then
                                PostProcessObject(obj)
                            End If
                            obj = New RawObject(objectType, subType.Value, rawReader.CurrentToken.Arguments(0), Me, source)
                            If obj.ObjectType = objectType.BODY AndAlso obj.ObjectSubType = ObjectSubType.BODYGLOSS Then
                                Dim gloss As String = rawReader.CurrentToken.GetArgumentOrDefault(0, "")
                                If Not mBodyGloss.ContainsKey(gloss) Then
                                    Dim entry As New BodyGlossEntry
                                    entry.GlossToken = rawReader.CurrentToken
                                    entry.Id = entry.GlossToken.Arguments(0)
                                    entry.Library = Me
                                    entry.Source = source
                                    mBodyGloss.Add(gloss, entry)
                                Else
                                    Dim issue As New RawIssue
                                    issue.Severity = IssueSeverity.Warning
                                    issue.Text = "Duplicate BODYGLOSS """ & gloss & """." & Environment.NewLine &
                                                  "Found in """ & DfHelper.MakeRelativePath(mBasePath, filename) & """."
                                    mIssues.Add(issue)
                                End If
                                skipObject = False
                            Else
                                skipObject = Not AddObject(obj)
                                If skipObject Then
                                    Dim originalObject As RawObject = GetObjectByType(obj.ObjectType, obj.objectId)
                                    Dim issue As New RawIssue
                                    issue.Severity = IssueSeverity.Error
                                    issue.Text = "Duplicate object """ & obj.objectId & """." & Environment.NewLine &
                                                  "Found in """ & DfHelper.MakeRelativePath(mBasePath, filename) & """." & Environment.NewLine &
                                                  "First found in """ & originalObject.Source.Path & """."
                                    mIssues.Add(issue)
                                End If
                            End If
                        Else
                            If obj IsNot Nothing Then
                                'data for the current object
                                If Not skipObject Then
                                    obj.Tokens.Add(rawReader.CurrentToken)
                                Else
                                    Debug.WriteLine("skipping data")
                                End If
                            Else
                                'token without object
                                'Stop
                            End If
                        End If
                        ProcessToken(rawReader.CurrentToken, obj)
                    Loop
                    If obj IsNot Nothing Then
                        PostProcessObject(obj)
                    End If
                Else
                    Debug.WriteLine("OBJECT TYPE MISMATCH: skipping file")
                End If
            End If
        End If
        reader.Close()

        SetUnsavedChangesFlag()
    End Sub

    Private Sub ProcessToken(token As Token, obj As RawObject)
        token.ReparseArgs(token.ArgumentsAsString, obj)
        If token.Name = "" Then
            Dim issue As New RawIssue
            issue.Severity = IssueSeverity.Warning
            issue.Text = "Token does not have a name." & vbNewLine &
                         "File: " & obj.Source.Path & vbNewLine &
                         "Object: " & obj.objectId & vbNewLine &
                         "Token: " & token.ToString
            mIssues.Add(issue)
        End If
        If obj IsNot Nothing Then
            Select Case obj.ObjectType
                Case ObjectType.GRAPHICS
                    Select Case token.Name
                        Case "TILE_PAGE"
                            If Not mGraphics.ContainsKey(token.Arguments(0)) Then
                                Dim gfxSet As GraphicsSet = Nothing
                                gfxSet = New GraphicsSet(token.Arguments(0))
                                mGraphics.Add(gfxSet.Id, gfxSet)
                            End If
                        Case "FILE"
                            Try
                                Dim path As String = IO.Path.Combine(mBasePath, IO.Path.Combine("graphics", token.Arguments(0)))
                                Dim image As Bitmap
                                If IO.File.Exists(path) Then
                                    image = New Bitmap(path)
                                Else
                                    image = New Bitmap(1, 1, Imaging.PixelFormat.Format32bppRgb)

                                    Dim issue As New RawIssue
                                    issue.Severity = IssueSeverity.Warning
                                    issue.Text = "Graphics image not found." & vbNewLine &
                                                 "File: " & obj.Source.Path
                                    mIssues.Add(issue)
                                End If
                                image.MakeTransparent()
                                mGraphics(obj.objectId).Graphics = image
                                mGraphics(obj.objectId).Path = token.Arguments(0)

                            Catch ex As Exception
                                Dim issue As New RawIssue
                                issue.Severity = IssueSeverity.Error
                                issue.Text = "The Graphics set """ & obj.objectId & """ specifies the image file """ & token.Arguments(0) & """, which could not be loaded."
                                mIssues.Add(issue)
                            End Try
                        Case "TILE_DIM"
                            mGraphics(obj.objectId).TileSize = New Size(token.ArgumentAsInteger(0, 12), token.ArgumentAsInteger(1, 12))
                        Case "PAGE_DIM"
                            mGraphics(obj.objectId).PageSize = New Size(token.ArgumentAsInteger(0, 1), token.ArgumentAsInteger(1, 1))
                    End Select
            End Select
        Else
            'Handle tokens that can show up outside of an object
        End If
    End Sub

    Private Sub PostProcessObject(obj As RawObject)
        Select Case obj.ObjectType
            Case ObjectType.LANGUAGE
                Select Case obj.ObjectSubType
                    Case ObjectSubType.WORD
                        mLanguage.AddWord(obj)
                    Case ObjectSubType.TRANSLATION
                        mLanguage.AddLanguage(obj.objectId)
                        For Each word As Token In obj.Tokens
                            If word.Name = "T_WORD" AndAlso word.Arguments.Count = 2 Then
                                mLanguage.Translation(obj.objectId, word.Arguments(0)) = word.Arguments(1)
                            End If
                        Next
                End Select
        End Select
    End Sub

    Public Function IsObjectIdInUse(objectId As String, objectType As ObjectType) As Boolean
        If mObjects.ContainsKey(objectType) Then
            If mObjects(objectType).ContainsKey(objectId) Then
                Return True
            Else
                Return False
            End If
        Else
            Return False
        End If
    End Function

    Public Function AddObject(obj As RawObject) As Boolean
        If Not mObjects.ContainsKey(obj.ObjectType) Then
            mObjects.Add(obj.ObjectType, New Dictionary(Of String, RawObject))
        End If
        If Not mObjects(obj.ObjectType).ContainsKey(obj.objectId) Then
            mObjects(obj.ObjectType).Add(obj.objectId, obj)
            obj.Library = Me
            Return True
        Else
            Return False
        End If
    End Function

    Public Sub AddSource(source As RawSource)
        If mSources.Contains(source) Then
            Throw New ArgumentException("This source is already in the library.")
        End If
        mSources.Add(source)
    End Sub

    Public Function GetObjectByType(objectType As ObjectType, objectId As String) As RawObject
        If mObjects.ContainsKey(objectType) Then
            If mObjects(objectType).ContainsKey(objectId) Then
                Return mObjects(objectType)(objectId)
            Else
                Return Nothing
            End If
        Else
            Return Nothing
        End If
    End Function

    Private Sub AddDescriptor(objectDescriptor As RawObjectDescriptor)
        ObjectDescriptors.Add(objectDescriptor.ObjectType, objectDescriptor)
    End Sub

    Public Function GetDescriptor(objectType As ObjectType) As RawObjectDescriptor
        If ObjectDescriptors.ContainsKey(objectType) Then
            Return ObjectDescriptors(objectType)
        Else
            Return Nothing
        End If
    End Function

    Public Function GetDescriptorFromTypeName(typeName As String) As RawObjectDescriptor
        Return (From desc As RawObjectDescriptor In ObjectDescriptors.Values Where desc.TypeName = typeName).FirstOrDefault
    End Function

    Public ReadOnly Property HasIssues As Boolean
        Get
            If mIssues.Count > 0 Then
                Return True
            Else
                Return False
            End If
        End Get
    End Property

    Public ReadOnly Property Issues As IEnumerable(Of RawIssue)
        Get
            Return mIssues
        End Get
    End Property

    Public Function HasCreatureGraphic(objectId As String) As Boolean
        If GetObjectByType(ObjectType.GRAPHICS, objectId) IsNot Nothing Then
            Return True
        Else
            Return False
        End If
    End Function

    Public Function GetGraphicSet(setName As String) As GraphicsSet
        Dim ret As GraphicsSet = Nothing
        mGraphics.TryGetValue(setName, ret)
        Return ret
    End Function

    ''' <summary>
    ''' Returns a spcific sub image from a grid of images specified by graphicSetId pulled from the current library
    ''' </summary>
    ''' <param name="config"></param>
    ''' <param name="graphicSetId">the graphic set to pull</param>
    ''' <param name="x">coords</param>
    ''' <param name="y">coords</param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetGraphicSetImage(config As Config, graphicSetId As String, x As Integer, y As Integer) As Bitmap
        'Dim ret As New Bitmap(config.tileSet.tileSize.Width, config.tileSet.tileSize.Height, Imaging.PixelFormat.Format32bppArgb)
        'Dim g As Graphics = Graphics.FromImage(ret)
        'g.Clear(Color.Black)

        'Dim graphicEntry As RawObject = getObjectByType(ObjectType.GRAPHICS, graphicSetId)
        'If graphicEntry IsNot Nothing Then

        '    Dim gfxSet As GraphicsSet = Nothing
        '    If mGraphics.TryGetValue(graphicSetId, gfxSet) Then
        '        Dim srcRect As New Rectangle(x * ret.Width, y * ret.Height, ret.Width, ret.Height)
        '        Dim destRect As New Rectangle(0, 0, ret.Width, ret.Height)
        '        g.DrawImage(gfxSet.graphics, destRect, srcRect, GraphicsUnit.Pixel)
        '    End If
        'End If

        'g.Dispose()

        'Return ret
        Return GetGraphicSetImage(mGraphics, config, graphicSetId, x, y)
    End Function

    ''' <summary>
    ''' Returns a spcific sub image from a grid of images specified by graphicSetId pulled from the passed dictionary of graphicSets
    ''' </summary>
    ''' <param name="graphicSource">A dictionary of image tiles keyed by the graphic set id</param>
    ''' <param name="config"></param>
    ''' <param name="graphicSetId">the graphic set to pull</param>
    ''' <param name="x">coords</param>
    ''' <param name="y">coords</param>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetGraphicSetImage(graphicSource As Dictionary(Of String, GraphicsSet), config As Config, graphicSetId As String, x As Integer, y As Integer) As Bitmap
        Dim ret As New Bitmap(config.TileSet.TileSize.Width, config.TileSet.TileSize.Height, Imaging.PixelFormat.Format32bppArgb)
        Dim g As Graphics = Graphics.FromImage(ret)
        g.Clear(Color.Black)

        'Dim graphicEntry As RawObject = getObjectByType(ObjectType.GRAPHICS, graphicSetId)
        'If graphicEntry IsNot Nothing Then

        Dim gfxSet As GraphicsSet = Nothing
        If graphicSource.TryGetValue(graphicSetId, gfxSet) Then
            Dim srcRect As New Rectangle(x * ret.Width, y * ret.Height, ret.Width, ret.Height)
            Dim destRect As New Rectangle(0, 0, ret.Width, ret.Height)
            g.DrawImage(gfxSet.Graphics, destRect, srcRect, GraphicsUnit.Pixel)
        End If
        'End If

        g.Dispose()

        Return ret
    End Function

    Public Function GetCreatureGraphic(obj As RawObject, config As Config) As Bitmap
        Dim objectId As String = obj.ObjectId
        Dim graphicEntry As RawObject = GetObjectByType(ObjectType.GRAPHICS, objectId)
        If graphicEntry IsNot Nothing Then
            Dim graphicToken As Token = Nothing ' = graphicEntry.getFirstToken("DEFAULT")
            Dim fallbackToken As Token = Nothing
            Dim index As Integer = 0
            Do Until graphicToken IsNot Nothing OrElse index >= mGraphicSearchList.Count
                graphicToken = graphicEntry.GetFirstToken(mGraphicSearchList(index))
                If fallbackToken Is Nothing AndAlso graphicToken IsNot Nothing Then
                    fallbackToken = graphicToken
                End If
                If graphicToken IsNot Nothing Then
                    If graphicToken.Arguments.Count >= 5 AndAlso graphicToken.Arguments(4) <> "DEFAULT" Then
                        graphicToken = Nothing
                    End If
                End If
                index += 1
            Loop
            If graphicToken Is Nothing Then
                graphicToken = fallbackToken
            End If

            If graphicToken IsNot Nothing Then
                Dim gfxSet As GraphicsSet = Nothing
                mGraphics.TryGetValue(graphicToken.Arguments(0), gfxSet)
                If gfxSet IsNot Nothing Then
                    Dim img As New Bitmap(gfxSet.TileSize.Width, gfxSet.TileSize.Height, Imaging.PixelFormat.Format32bppArgb)
                    Dim srcRect As New Rectangle(graphicToken.ArgumentAsInteger(1) * gfxSet.TileSize.Width, graphicToken.ArgumentAsInteger(2) * gfxSet.TileSize.Height, gfxSet.TileSize.Width, gfxSet.TileSize.Height)
                    Dim destRect As New Rectangle(0, 0, gfxSet.TileSize.Width, gfxSet.TileSize.Height)
                    Dim g As Graphics = Graphics.FromImage(img)
                    g.Clear(Color.Black)
                    g.DrawImage(gfxSet.Graphics, destRect, srcRect, GraphicsUnit.Pixel)
                    g.Dispose()

                    'Apply color if needed (and passed)
                    If graphicToken.GetArgumentOrDefault(3, "AS_IS") = "ADD_COLOR" Then
                        Dim colorToken As Token = obj.GetFirstToken("COLOR", True)
                        If colorToken IsNot Nothing Then
                            ColorHelper.Colorize(img, config.ColorSet.GetForeColorFromTriplet(colorToken, obj))
                        End If
                    End If

                    Return img
                Else
                    Dim img As New Bitmap(config.TileSet.TileSize.Width, config.TileSet.TileSize.Height, Imaging.PixelFormat.Format32bppArgb)
                    Dim g As Graphics = Graphics.FromImage(img)
                    g.Clear(Color.Black)
                    g.Dispose()
                    Return img
                End If
            End If
        Else
            Dim ret As New Bitmap(16, 16, Imaging.PixelFormat.Format32bppArgb)
            Dim gr As Graphics = Graphics.FromImage(ret)
            gr.Clear(Color.White)
            gr.DrawLine(Pens.Red, 0, 0, ret.Width, ret.Height)
            gr.DrawLine(Pens.Red, 0, ret.Height, ret.Width, 0)
            gr.Dispose()
        End If
        Return Nothing
    End Function

    Public Function GetAllObjects() As IEnumerable(Of RawObject)
        Dim ret As New List(Of RawObject)

        For Each objTypeKey As ObjectType In mObjects.Keys
            Dim category As Dictionary(Of String, RawObject) = mObjects(objTypeKey)
            For Each obj As RawObject In category.Values
                ret.Add(obj)
            Next
        Next

        Return ret
    End Function

    Public Function GetObjectsByType(objectType As ObjectType) As IEnumerable(Of RawObject)
        Dim category As Dictionary(Of String, RawObject)
        If mObjects.ContainsKey(objectType) Then
            category = mObjects(objectType)

            Return category.Values
        Else
            Return New List(Of RawObject)
        End If
    End Function

    Public Function GetObjectsByType(objectType As ObjectType, subType As ObjectSubType) As IEnumerable(Of RawObject)
        Dim category As Dictionary(Of String, RawObject)
        If mObjects.ContainsKey(objectType) Then
            category = mObjects(objectType)

            Return (From obj As RawObject In category.Values Where obj.ObjectSubType = subType)
        Else
            Return New List(Of RawObject)
        End If
    End Function

    Public Shared Function GetSubTypeFromString(subType As String) As ObjectSubType?
        Dim ret As ObjectSubType
        If [Enum].TryParse(Of ObjectSubType)(subType, ret) Then
            Return ret
        Else
            Return Nothing
        End If
    End Function

    Public Sub ExportTokenList(file As String)
        Dim TokenList As New Dictionary(Of String, TokenExportInfo)

        For Each obj As RawObject In Me.GetAllObjects
            For Each Token As Token In obj.Tokens
                If Not TokenList.ContainsKey(Token.Name) Then
                    TokenList.Add(Token.Name, New TokenExportInfo(Token.Name))
                End If
                TokenList(Token.Name).addUse(obj.ObjectType.ToString & ":" & obj.ObjectSubType.ToString)
            Next
        Next

        Dim sortedList As List(Of TokenExportInfo) = TokenList.Values.ToList
        sortedList.Sort()

        Dim stream As New IO.StreamWriter(file, False)
        For Each entry As TokenExportInfo In sortedList
            stream.WriteLine(entry.Name)
            stream.WriteLine(vbTab & String.Join(","c, entry.UsedOn.ToList))
        Next
        stream.Close()
    End Sub

    Private Class TokenExportInfo
        Implements IComparable(Of TokenExportInfo)

        Public Property Name As String
        Public Property UsedOn As HashSet(Of String)

        Public Sub New(name As String)
            Me.Name = name
            UsedOn = New HashSet(Of String)
        End Sub

        Public Sub addUse(usedOn As String)
            If Not Me.UsedOn.Contains(usedOn) Then
                Me.UsedOn.Add(usedOn)
            End If
        End Sub

        Public Function CompareTo(other As TokenExportInfo) As Integer Implements System.IComparable(Of TokenExportInfo).CompareTo
            Return String.Compare(Me.Name, other.Name)
        End Function
    End Class

    Public Function GetObjectIdsByType(objectType As ObjectType, subType As ObjectSubType) As System.Collections.Generic.IEnumerable(Of String)
        Return (From obj As RawObject In GetObjectsByType(objectType, subType) Select obj.objectId)
    End Function

    Public Function GetObjectsByTypeCode(type As TypeCode) As IEnumerable(Of RawObject)
        Return Me.GetAllObjects.Where(Function(obj As RawObject) type.Matches(obj.TypeCode))
    End Function

    Public ReadOnly Property Sources As IEnumerable(Of RawSource)
        Get
            Return mSources
        End Get
    End Property

    Public Function GetObjectsInSource(source As RawSource) As IEnumerable(Of RawObject)
        If source.Library IsNot Me Then
            Throw New ArgumentException("source is not a member of this library.")
        End If

        Return (From rawObj As RawObject In GetAllObjects() Where rawObj.Source Is source)
    End Function

    Public Sub DeleteObject(obj As RawObject)
        mObjects(obj.ObjectType).Remove(obj.objectId)
        SetUnsavedChangesFlag()
    End Sub


    Public Sub RemoveSource(source As RawSource)
        If source.GetObjects.Count > 0 Then
            Throw New ArgumentException("source must be empty before it can be removed")
        End If
        RemoveSource(source, False)
    End Sub

    Public Sub RemoveSource(source As RawSource, removeObjects As Boolean)
        If removeObjects Then
            Dim objects As New List(Of RawObject)(source.GetObjects)
            For Each obj As RawObject In objects
                Me.RemoveObject(obj)
            Next
        Else
            If source.GetObjects.Count > 0 Then
                Throw New ArgumentException("source must be empty before it can be removed")
            End If
        End If
        mSources.Remove(source)
        SetUnsavedChangesFlag()
    End Sub

    Public Sub RemoveObject(obj As RawObject)
        If mObjects.ContainsKey(obj.ObjectType) Then
            Dim typeList As Dictionary(Of String, RawObject) = mObjects(obj.ObjectType)
            If typeList.ContainsKey(obj.objectId) Then
                typeList.Remove(obj.objectId)
                obj.Library = Nothing
                obj.Source = Nothing
            Else
                Throw New ArgumentException("Object is not a member of this library.")
            End If
        End If
    End Sub

    Public Sub Save(fileName As String)
        Dim stream As FileStream = Nothing
        Dim oldName As String = Me.libraryName
        Try
            Me.libraryName = fileName
            stream = New FileStream(fileName, FileMode.OpenOrCreate)
            Dim formatter As New BinaryFormatter

            formatter.Serialize(stream, Me)
            ClearUnsavedChangesFlag()
        Catch ex As Exception
            Me.libraryName = oldName
            Throw
        Finally
            If stream IsNot Nothing Then
                stream.Close()
                stream.Dispose()
            End If
        End Try
    End Sub

    Public Shared Function FromFile(fileName As String) As RawLibrary
        Dim ret As RawLibrary

        Dim stream As FileStream = Nothing
        Try
            stream = New FileStream(fileName, FileMode.Open)

            Dim formatter As New BinaryFormatter
            ret = CType(formatter.Deserialize(stream), RawLibrary)
        Finally
            If stream IsNot Nothing Then
                stream.Close()
                stream.Dispose()
            End If
        End Try

        ret.ClearUnsavedChangesFlag()

        Return ret
    End Function

    Private mHasUnsavedChanges As Boolean
    Public ReadOnly Property HasUnsavedChanged As Boolean
        Get
            Return mHasUnsavedChanges
        End Get
    End Property

    Public Sub ClearUnsavedChangesFlag()
        mHasUnsavedChanges = False
    End Sub

    Public Sub SetUnsavedChangesFlag()
        mHasUnsavedChanges = True
    End Sub

    Public Sub ExportRaws(directory As String, rawName As String, options As ExportOptions)
        If Not IO.Directory.Exists(directory) Then
            Throw New ArgumentException("Directory does not exist")
        End If

        Dim stream As FileStream = Nothing

        Try
            Dim basePath As String = IO.Path.Combine(directory, rawName)
            If options.DeleteExistingDirectory AndAlso IO.Directory.Exists(basePath) Then
                DfHelper.DeleteDirectory(basePath)
            End If

            For Each source As RawSource In mSources
                Dim rawFile As String = IO.Path.Combine(basePath, source.Path)
                Dim rawFolderPath As String = IO.Path.GetDirectoryName(rawFile)
                If Not IO.Directory.Exists(rawFolderPath) Then
                    IO.Directory.CreateDirectory(rawFolderPath)
                End If

                stream = New FileStream(rawFile, FileMode.OpenOrCreate)
                Dim writer As New StreamWriter(stream)

                source.WriteToStream(writer, Me, options)

                writer.Flush()
                stream.Close()
                stream.Dispose()
            Next

            If Not IO.Directory.Exists(IO.Path.Combine(basePath, "graphics")) Then
                IO.Directory.CreateDirectory(IO.Path.Combine(basePath, "graphics"))
            End If
            For Each gr As GraphicsSet In mGraphics.Values
                Dim imagePath As String = IO.Path.Combine(basePath, IO.Path.Combine("graphics", gr.Path))
                MakeDirectoryFor(imagePath)

                stream = New FileStream(imagePath, FileMode.OpenOrCreate)
                gr.Graphics.Save(stream, Imaging.ImageFormat.Png)
                stream.Close()
                stream.Dispose()
            Next
        Catch ex As Exception
            Throw
        Finally
            If stream IsNot Nothing Then
                stream.Close()
                stream.Dispose()
            End If
        End Try
    End Sub

    Private Sub MakeDirectoryFor(fullFilePath As String)
        Dim dir As String = IO.Path.GetDirectoryName(fullFilePath)
        If Not IO.Directory.Exists(dir) Then
            MakeDirectoryFor(dir)
            IO.Directory.CreateDirectory(dir)
        End If
    End Sub

    Public Function GetRawObjectById(objectId As String) As RawObject
        For Each obj As RawObject In Me.GetAllObjects
            If obj.objectId = objectId Then
                Return obj
            End If
        Next
        Return Nothing
    End Function

    Public Function GetObjectById(objectId As String) As RawObject
        For Each obj As RawObject In Me.GetAllObjects
            If obj.objectId = objectId Then
                Return obj
            End If
        Next
        Return Nothing
    End Function

    Public Function InferTypeFromSubType(subType As ObjectSubType) As ObjectType
        For Each desc As RawObjectDescriptor In ObjectDescriptors.Values
            If desc.ObjectClasses.Contains(subType) Then
                Return desc.ObjectType
            End If
        Next
        Return ObjectType.ALL
    End Function

    Public Function GetUniqueObjectIdFor(obj As RawObject) As String
        Dim name As String = obj.objectId
        Dim index As Integer = 1
        Do Until Me.GetRawObjectById(name) Is Nothing
            index += 1
            name = obj.objectId & index.ToString
        Loop
        Return name
    End Function

    Public Function GetSources(objectType As ObjectType) As List(Of RawSource)
        Return New List(Of RawSource)(From s As RawSource In mSources Where s.ObjectType = objectType)
    End Function

    Private Function GetObjectsByTypeCode1(type As TypeCode) As IEnumerable(Of RawObject)
        Return GetObjectsByTypeCode(type)
    End Function

    Public Function GetBodyGlossary() As System.Collections.Generic.IEnumerable(Of String)
        Return New List(Of String)(mBodyGloss.Keys)
    End Function

    Public ReadOnly Property BodyGlossary As IEnumerable(Of BodyGlossEntry)
        Get
            Return mBodyGloss.Values
        End Get
    End Property

    Public Sub UpdateBodyglossary(data As IEnumerable(Of BodyGlossEntry))
        mBodyGloss.Clear()
        For Each entry As BodyGlossEntry In data
            mBodyGloss.Add(entry.Id, entry)
        Next
    End Sub

    Public Function GetImageForObject(obj As RawObject, config As Config) As Image
        Dim ret As Bitmap = Nothing
        ' Dim gr As Graphics = Graphics.FromImage(ret)

        Select Case obj.ObjectType
            Case ObjectType.CREATURE
                If Me.HasCreatureGraphic(obj.objectId) Then
                    Dim img As Image = Me.getCreatureGraphic(obj, config)
                    If img IsNot Nothing Then
                        ret = New Bitmap(img.Width, img.Height, Imaging.PixelFormat.Format32bppArgb)
                        Dim gr As Graphics = Graphics.FromImage(ret)
                        If img IsNot Nothing Then
                            gr.Clear(Color.Black)
                            gr.DrawImage(img, 0, 0)
                            img.Dispose()
                        End If
                        gr.Dispose()
                    End If
                Else
                    Dim tileToken As Token = obj.GetFirstToken("CREATURE_TILE", True)
                    If tileToken IsNot Nothing Then
                        Dim tile As Bitmap
                        If tileToken.Arguments(0).StartsWith("'") OrElse tileToken.Arguments(0).StartsWith("""") Then
                            Dim c As Char = "a"c
                            If tileToken.Arguments(0).Length > 1 Then
                                c = tileToken.Arguments(0)(1)
                            End If
                            tile = config.TileSet.GetTile(c)
                        Else
                            Dim num As Integer = 0
                            Integer.TryParse(tileToken.Arguments(0), num)
                            tile = config.TileSet.GetTile(num)
                        End If
                        ret = New Bitmap(tile.Width, tile.Height, Imaging.PixelFormat.Format32bppArgb)
                        Dim gr As Graphics = Graphics.FromImage(ret)
                        Dim colorToken As Token = obj.GetFirstToken("COLOR", True)
                        If colorToken IsNot Nothing Then
                            gr.Clear(config.ColorSet.GetBackColorFromTriplet(colorToken, obj))
                            ColorHelper.Colorize(tile, config.ColorSet.GetForeColorFromTriplet(colorToken, obj))
                        Else
                            gr.Clear(Color.Black)
                        End If
                        gr.DrawImage(tile, 0, 0)
                        tile.Dispose()
                        gr.Dispose()
                    End If
                End If
            Case ObjectType.PLANT
                Dim tileToken As Token = obj.GetFirstTokenOf({"TREE_TILE", "SHRUB_TILE", "SAPLING_TILE", "DEAD_TREE_TILE", "PICKED_TILE", "DEAD_SHRUB_TILE", "GRASS_TILES"}, True)
                If tileToken IsNot Nothing Then
                    Dim colorTokenNameBase As String = tileToken.Name.Substring(0, tileToken.Name.LastIndexOf("_"c))

                    Dim colorToken As Token = obj.GetFirstTokenOf({colorTokenNameBase & "_COLOR", colorTokenNameBase & "_COLORS"}, True)
                    Dim tile As Bitmap
                    If tileToken.Arguments(0).StartsWith("'") OrElse tileToken.Arguments(0).StartsWith("""") Then
                        tile = config.TileSet.GetTile(tileToken.Arguments(0)(1))
                    Else
                        tile = config.TileSet.GetTile(CInt(tileToken.Arguments(0)))
                    End If
                    ret = New Bitmap(tile.Width, tile.Height, Imaging.PixelFormat.Format32bppArgb)
                    Dim gr As Graphics = Graphics.FromImage(ret)
                    If colorToken IsNot Nothing Then
                        gr.Clear(config.ColorSet.GetBackColorFromTriplet(colorToken, obj))
                        ColorHelper.Colorize(tile, config.ColorSet.GetForeColorFromTriplet(colorToken, obj))
                    Else
                        gr.Clear(Color.Black)
                    End If
                    gr.DrawImage(tile, 0, 0)
                    tile.Dispose()
                    gr.Dispose()
                Else
                    ret = My.Resources.rawObject
                End If
            Case ObjectType.DESCRIPTOR_COLOR
                Dim rgbToken As Token = obj.GetFirstTokenOf({"RGB"}, True)
                If rgbToken IsNot Nothing AndAlso rgbToken.Arguments.Count = 3 Then
                    Dim r As Integer = Math.Max(Math.Min(rgbToken.ArgumentAsInteger(0, 0), 255), 0)
                    Dim g As Integer = Math.Max(Math.Min(rgbToken.ArgumentAsInteger(1, 0), 255), 0)
                    Dim b As Integer = Math.Max(Math.Min(rgbToken.ArgumentAsInteger(2, 0), 255), 0)
                    Dim swatchColor As Color = Color.FromArgb(r, g, b)
                    Dim swatch As New Bitmap(config.TileSet.TileSize.Width, config.TileSet.TileSize.Height, Imaging.PixelFormat.Format24bppRgb)
                    Using gr As Graphics = Graphics.FromImage(swatch), br As New SolidBrush(swatchColor)
                        gr.FillRectangle(br, New Rectangle(0, 0, swatch.Width - 1, swatch.Height - 1))
                        gr.DrawRectangle(Pens.Black, New Rectangle(0, 0, swatch.Width - 1, swatch.Height - 1))
                    End Using
                    ret = swatch
                Else
                    ret = My.Resources.rawObject
                End If
            Case Else
                ret = My.Resources.rawObject
        End Select

        If ret Is Nothing Then
            ret = New Bitmap(1, 1, Imaging.PixelFormat.Format32bppArgb)
            Dim gr As Graphics = Graphics.FromImage(ret)
            gr.Clear(Color.Transparent)
            gr.Dispose()
        End If

        Return ret
    End Function

    Public ReadOnly Property Language As LanguageLookup
        Get
            Return mLanguage
        End Get
    End Property

    Public Sub UpdateLanguage(language As LanguageLookup)
        'TODO: Implement UpdateLanguage
        'replace the language lookup, and replace the actual words in the library

        'Step 1: remove all current language sources (and their objects) from the library
        Dim sources As New List(Of RawSource)(From s As RawSource In mSources Where s.ObjectType = ObjectType.LANGUAGE)
        For Each source As RawSource In sources
            Me.RemoveSource(source, True)
        Next

        'Step 2: create new sources
        'Step 2a: words
        Dim wordSource As New RawSource("language_words.txt", Me, ObjectType.LANGUAGE)
        Me.AddSource(wordSource)
        For Each word As String In language.Words
            Dim obj As RawObject = language.GetWordObject(word)
            obj.Source = wordSource
            Me.AddObject(obj)
        Next

        'Step 2b: languages
        For Each lang As String In language.Languages
            Dim source As New RawSource("language_" & lang & ".txt", Me, ObjectType.LANGUAGE)
            mSources.Add(source)

            Dim langObj As New RawObject(ObjectType.LANGUAGE, ObjectSubType.TRANSLATION, lang, Me, source)
            Me.AddObject(langObj)
            For Each word As String In language.Words
                langObj.AddToken("T_WORD", String.Format("{0}:{1}", word, language.Translation(lang, word)))
            Next
        Next

        'Step 2c:symbols


        'Step 3: switch references
        mLanguage = language
    End Sub

    Public Function GetEntityFor(creatureId As String) As RawObject
        For Each entity As RawObject In GetObjectsByType(ObjectType.ENTITY, ObjectSubType.ENTITY)
            For Each Token As Token In entity.GetAllTokens("CREATURE")
                If Token.GetArgumentOrDefault(0, "") = creatureId Then
                    Return entity
                End If
            Next
        Next
        Return Nothing
    End Function

    ''' <summary>
    ''' Returns a copy of the dictionary containing the currently loaded graphic sets.
    ''' </summary>
    ''' <returns></returns>
    ''' <remarks></remarks>
    Public Function GetGraphicSetsCopy() As Dictionary(Of String, GraphicsSet)
        Return New Dictionary(Of String, GraphicsSet)(mGraphics)
    End Function

    ''' <summary>
    ''' Removed all objects of the specified type from the library
    ''' </summary>
    ''' <param name="objectType">The objectType to remove</param>
    ''' <remarks></remarks>
    Public Sub RemoveAllObjectsByType(objectType As ObjectType)
        If objectType = objectType.ALL Then
            Throw New ArgumentException("ObjectType.ALL is not supported.")
        Else
            mObjects(objectType).Clear()
        End If
    End Sub

    Public Sub ClearGraphicSets()
        mGraphics.Clear()
    End Sub

    Public Sub AddGraphicSet(graphicSetId As String, data As GraphicsSet)
        mGraphics.Add(graphicSetId, data)
    End Sub

End Class
